﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MethodWorx.Core.DomainModel;
using MethodWorx.CMS.Web;
using MethodWorx.CMS.Providers;
using MethodWorx.CMS.Configuration;
using MethodWorx.CMS.Plugins;
using MethodWorx.CMS.Content;
using MethodWorx.CMS.Requests;
using MethodWorx.Core.Validation;
using MethodWorx.CMS.Administration;

namespace MethodWorx.CMS.Services
{
    public class ContentService : IContentService
    {
        private IPageTemplateRepository pageTemplates;
        private IContentTemplateRepository contentTemplates;
        private IPageRepository pages;
        private IContentItemRepository contentItems;
        private IUnitOfWorkFactory uowFactory;
        private ITemplateCompilationProvider compilationProvider;
        private PluginFactory pluginFactory;

        /// <summary>
        /// Constructor with services injeted fro the ICmsDataProvider
        /// </summary>
        /// <param name="requestContext"></param>
        /// <param name="provider"></param>
        public ContentService(ICmsDataProvider provider)
            : this( provider.GetService<IPageTemplateRepository>(), 
                    provider.GetService<IContentTemplateRepository>(),
                    provider.GetService<IPageRepository>(),
                    provider.GetUnitOfWorkFactory(),
                    provider.GetService<IContentItemRepository>(),
                    provider.GetService<ITemplateCompilationProvider>(),
                    provider.GetService<IApplicationSettingsRepository>(),
                    provider.GetService<IPluginRepository>()
            )
        {
        }

        /// <summary>
        /// Constructor with services injected from a DI Container or consumer
        /// </summary>
        /// <param name="pageTemplates"></param>
        /// <param name="pages"></param>
        /// <param name="uowFactory"></param>
        public ContentService(IPageTemplateRepository pageTemplates, IContentTemplateRepository contentTemplates, IPageRepository pages, IUnitOfWorkFactory unitOfWorkFactory, IContentItemRepository contentItems, ITemplateCompilationProvider compilationProvider, IApplicationSettingsRepository applicationSettings, IPluginRepository plugins)
        {
            this.pluginFactory = new PluginFactory(applicationSettings, plugins);
            this.pageTemplates = pageTemplates;
            this.pages = pages;
            this.contentItems = contentItems;
            this.uowFactory = unitOfWorkFactory;
            this.contentTemplates = contentTemplates;
            this.compilationProvider = compilationProvider;
        }

        public Page CreateRootPage(string name, string description, PageTemplate pageTemplate)
        {
            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            if (pageTemplate == null)
                throw new ArgumentNullException("pageTemplate");

            //  ok, lets start
            using (IUnitOfWork uow = this.uowFactory.Begin())
            {
                Page page = this.pages.CreateEntityInstance();

                //
                //  ok, now set it up
                page.Name = name;
                page.Description = description;
                page.Parent = null;
                page.Template = pageTemplate;

                uow.RegisterAdded(page);

                uow.Commit();

                return page;
            }

        }

        public Page GetRootPage()
        {
            return this.pages.FindRoot();
        }

        public Page GetPageById(Guid id)
        {
            return this.pages[id];
        }

        public ContentItem UpdateContentItemProperties(Guid id, IEnumerable<IPropertyDetails> propertyDetails)
        {
            if (propertyDetails == null)
                throw new ArgumentNullException("propertyDetails");

            var contentItem = this.contentItems[id];
            using (var unitOfWork = this.uowFactory.Begin())
            {
                List<PageProperty> properties = new List<PageProperty>();
                foreach (IPropertyDetails detail in propertyDetails)
                {
                    properties.Add(new PageProperty()
                    {
                        Name = detail.Name,
                        Value = (ICustomPropertyDetails)detail.Custom
                    });
                }
                contentItem.Properties = properties.ToArray();

                unitOfWork.RegisterChanged(contentItem);
                unitOfWork.Commit();
            }
            return contentItem;
        }

        public ContentItem UpdateContentItemProperties(Guid id, IDictionary<string, ICustomPropertyDetails> propertyDetails)
        {
            if (propertyDetails == null)
                throw new ArgumentNullException("propertyDetails");

            var contentItem = this.contentItems[id];
            using (var unitOfWork = this.uowFactory.Begin())
            {
                List<PageProperty> properties = new List<PageProperty>();
                foreach (string detail in propertyDetails.Keys)
                {
                    properties.Add(new PageProperty()
                    {
                        Name = detail,
                        Value = propertyDetails[detail]
                    });
                }
                contentItem.Properties = properties.ToArray();

                unitOfWork.RegisterChanged(contentItem);
                unitOfWork.Commit();
            }
            return contentItem;
        }

        public Page UpdatePageProperties(Guid id, IDictionary<string, ICustomPropertyDetails> propertyDetails)
        {
            if (propertyDetails == null)
                throw new ArgumentNullException("propertyDetails");

            var page = this.pages[id];
            using (var unitOfWork = this.uowFactory.Begin())
            {
                List<PageProperty> properties = new List<PageProperty>();
                foreach (string detail in propertyDetails.Keys)
                {
                    properties.Add(new PageProperty()
                    {
                        Name = detail,
                        Value = propertyDetails[detail]
                    });
                }
                page.Properties = properties.ToArray();

                unitOfWork.RegisterChanged(page);
                unitOfWork.Commit();
            }
            return page;
        }



        public Page UpdatePageProperties(Guid id, IEnumerable<IPropertyDetails> propertyDetails)
        {
            if (propertyDetails == null)
                throw new ArgumentNullException("propertyDetails");

            var page = this.pages[id];
            using (var unitOfWork = this.uowFactory.Begin())
            {
                List<PageProperty> properties = new List<PageProperty>();
                foreach (IPropertyDetails detail in propertyDetails)
                {
                    properties.Add(new PageProperty()
                    {
                        Name = detail.Name,
                        Value = (ICustomPropertyDetails)detail.Custom
                    });
                }
                page.Properties = properties.ToArray();

                unitOfWork.RegisterChanged(page);
                unitOfWork.Commit();
            }
            return page;
        }

        public IEnumerable<Page> GetAllPages()
        {
            return this.pages.All();
        }

    
        public void DeletePage(Page page)
        {
            if(page == null)
                throw new ArgumentNullException("page");


            using (var uow = this.uowFactory.Begin())
            {
                //  delete the pages recursivley
                this.DeletePageRecursive(uow, page);
                //
                //  now, delete the page
                uow.Commit();
            }
        }

        public void DeleteContentItem(ContentItem contentItem)
        {
            if(contentItem == null)
                throw new ArgumentNullException("contentItem");

            using(var uow = this.uowFactory.Begin())
            {
                if(contentItem.Status == ContentStatus.Unpublished)
                {
                    contentItem.Status = ContentStatus.AwaitingDelete;
                }
                else if(contentItem.Status == ContentStatus.Published)
                {
                    contentItem.Status = ContentStatus.PublishedAndAwaitingDelete;
                }
                uow.RegisterChanged(contentItem);
                uow.Commit();
            }
        }

        private void DeletePageRecursive(IUnitOfWork uow, Page page)
        {
            foreach (var childPage in page.ChildPages)
            {
                DeletePageRecursive(uow, childPage);
            }

            //
            //  delete the page
            if (page.Status == ContentStatus.Unpublished)
            {
                page.Status = ContentStatus.AwaitingDelete;
            }
            else if (page.Status == ContentStatus.Published)
            {
                page.Status = ContentStatus.PublishedAndAwaitingDelete;
            }
            uow.RegisterChanged(page);

            //
            //  delete the content items
            foreach (var ci in this.contentItems.FindByPage(page))
            {
                //  and delete the content item
                this.DeleteContentItem(ci);
            }
        }

        /// <summary>
        /// Create a content item
        /// </summary>
        /// <param name="parent"></param>
        /// <param name="templateName"></param>
        /// <param name="name"></param>
        /// <param name="autoIncrement"></param>
        /// <returns></returns>
        public ContentItem CreateContentItem(Page parent, string templateName, string name, bool autoIncrement)
        {
            var template = this.contentTemplates.FindByName(name);
            if (template == null)
                throw new BusinessRuleException("Template: " + template + " does not exist");

            return this.CreateContentItem(parent, template, name, autoIncrement);
        }

        /// <summary>
        /// Create a content item
        /// </summary>
        /// <param name="parent"></param>
        /// <param name="template"></param>
        /// <param name="name"></param>
        /// <param name="autoIncrement"></param>
        /// <returns></returns>
        public ContentItem CreateContentItem(Page parent, ContentTemplate template, string name, bool autoIncrement)
        {
            using(var uow = this.uowFactory.Begin())
            {
                if (autoIncrement)
                    name = GetAutoIncrementedContentItemName(parent, name);
                else if (parent.ChildContent.Where(c => c.Name.ToLower() == name.ToLower()).Any())
                    throw new BusinessRuleException("Can not use duplicate content item name: " + name);


                if (!parent.CanCreateContentItemFromTemplate(template))
                    throw new BusinessRuleException("Can not create content template due to constraints");

                //
                //  we can create the content item, so lets do it
                var contentItem = this.contentItems.CreateEntityInstance();

                //  and set it all up
                contentItem.Page = parent;
                contentItem.Template = template;
                contentItem.Name = name;

                uow.RegisterAdded(contentItem);


                uow.Commit();

                return contentItem;
            }
        }

        /// <summary>
        /// Create a content item
        /// </summary>
        /// <param name="parent"></param>
        /// <param name="templateName"></param>
        /// <param name="name"></param>
        /// <param name="autoIncrement"></param>
        /// <returns></returns>
        public Page CreatePage(Page parent, Guid templateId, string name, bool autoIncrement, bool publish)
        {
            var template = this.pageTemplates[templateId];
            if (template == null)
                throw new BusinessRuleException("Template: " + template + " does not exist");

            return this.CreatePage(parent, template, name, autoIncrement, publish);
        }

        public Page CreatePage(Page parent, PageTemplate template, string name, bool autoIncrement, bool publish)
        {
            using (var uow = this.uowFactory.Begin())
            {
                if (autoIncrement)
                    name = GetAutoIncrementedPageName(parent, name);
                else if (parent.ChildPages.Where(c => c.Name.ToLower() == name.ToLower()).Any())
                    throw new BusinessRuleException("Can not use duplicate content item name: " + name);


                if (!parent.CanCreatePageFromTemplate(template))
                    throw new BusinessRuleException("Can not create content template due to constraints");

                //
                //  we can create the content item, so lets do it
                var page = this.pages.CreateEntityInstance();

                //  and set it all up
                page.Parent = parent;
                page.Template = template;
                page.Name = name;

                uow.RegisterAdded(page);

                if (publish)
                    this.PublishPage(page.Id);

                uow.Commit();

                return page;
            }
        }

        private static string GetAutoIncrementedContentItemName(Page parent, string name)
        {
            bool found = false;
            int index = 1;
            while (!found)
            {
                name = name + index.ToString();
                if (parent.ChildContent.Where(c => c.Name.ToLower() == name.ToLower()).Any())
                    index++;
                else
                    found = true;
            }
            return name;
        }



        private static string GetAutoIncrementedPageName(Page parent, string name)
        {
            bool found = false;
            int index = 1;
            while (!found)
            {
                name = name + index.ToString();
                if (parent.ChildPages.Where(c => c.Name.ToLower() == name.ToLower()).Any())
                    index++;
                else
                    found = true;
            }
            return name;
        }


        public IEnumerable<ContentItem> CreateContentItem(CreateContentItemRequest request)
        {
            //
            //  ok, lets go for this then
            List<ContentItem> created = new List<ContentItem>();
            using (var uow = this.uowFactory.Begin())
            {
                ContentTemplate theTemplate;
                //
                //  lets, attempt to create the page template first
                if (!string.IsNullOrEmpty((request.NewTemplateName ?? "").Trim()))
                {
                    theTemplate = this.contentTemplates.CreateEntityInstance();
                    theTemplate.Name = request.NewTemplateName;

                    //
                    //  say we have changed the parent page and the request
                    uow.RegisterAdded(theTemplate);

                    if (request.ParentPage != null)
                    {

                        //
                        //  add the constraint in
                        request.ParentPage.Template.ContentTemplateConstraints.Add(theTemplate, request.MinimumRequired, request.MaximumAllowed, null);
                        uow.RegisterChanged(request.ParentPage.Template);
                    }
                }
                else
                {
                    theTemplate = request.ExistingContentTemplate;
                    if (request.ParentPage != null)
                    {
                        var constraint = request.ParentPage.Template.ContentTemplateConstraints.FirstOrDefault(t => t.RelatedTemplate.Id == theTemplate.Id);
                        if (constraint != null)
                        {
                            constraint.MinimumRequired = request.MinimumRequired;
                            constraint.MaximumAllowed = request.MaximumAllowed;
                        }
                        else
                        {

                            //
                            //  add the constraint in
                            request.ParentPage.Template.ContentTemplateConstraints.Add(theTemplate, request.MinimumRequired, request.MaximumAllowed, null);
                        }

                        //
                        //  say we have changed the parent page and the request
                        uow.RegisterChanged(request.ParentPage.Template);
                    }
                }

                

                var minimumRequired = request.MinimumRequired ?? 1;
                if (!request.CreateAllRequired)
                    minimumRequired = 1;
                for (var i = 1; i <= minimumRequired; i++)
                {

                    ContentItem contentItem = this.contentItems.CreateEntityInstance();
                    if (request.CreateAllRequired)
                        contentItem.Name = request.ContentName + i.ToString();
                    else
                        contentItem.Name = request.ContentName;

                    contentItem.Template = theTemplate;
                    contentItem.Page = request.ParentPage;
                    if (request.ParentPage.ChildContent.Any())
                        contentItem.Sequence = request.ParentPage.ChildContent.Max(c => c.Sequence) + 1;
                    else
                        contentItem.Sequence = 0;

                    //  create the page
                    uow.RegisterAdded(contentItem);

                    created.Add(contentItem);
                }


                //  commit the unit of work
                uow.Commit();

                return created;
            }


            return null;
        }

    

        public Page CreatePage(CreatePageRequest request, IValidationProvider validation)
        {
            //
            //  we need to do some funky validation here
            //  to see how many are actually published here
            this.ValidatePublishedPageTemplates(request, validation);

            if (validation.IsValid())
            {
                //
                //  ok, lets go for this then
                using (var uow = this.uowFactory.Begin())
                {
                    PageTemplate theTemplate;
                    //
                    //  lets, attempt to create the page template first
                    if (!string.IsNullOrEmpty((request.NewTemplateName ?? "").Trim()))
                    {
                        theTemplate = this.pageTemplates.CreateEntityInstance();
                        theTemplate.Abstract = false;
                        theTemplate.AllowedAtRoot = (request.ParentPage == null);
                        theTemplate.Name = request.NewTemplateName;
                        theTemplate.Parent = request.ParentTemplate;

                        //
                        //  say we have changed the parent page and the request
                        uow.RegisterAdded(theTemplate);

                        if (request.ParentPage != null)
                        {

                            //
                            //  add the constraint in
                            request.ParentPage.Template.PageTemplateConstraints.Add(theTemplate, request.MinimumRequired,
                                                                                    request.MaximumAllowed, null);
                            uow.RegisterChanged(request.ParentPage.Template);
                        }
                    }
                    else
                    {
                        theTemplate = request.ExistingPageTemplate;
                        if (request.ParentPage != null)
                        {
                            var constraint =
                                request.ParentPage.Template.PageTemplateConstraints.FirstOrDefault(
                                    t => t.RelatedTemplate.Id == theTemplate.Id);
                            if (constraint != null)
                            {
                                constraint.MinimumRequired = request.MinimumRequired;
                                constraint.MaximumAllowed = request.MaximumAllowed;
                            }
                            else
                            {

                                //
                                //  add the constraint in
                                request.ParentPage.Template.PageTemplateConstraints.Add(theTemplate,
                                                                                        request.MinimumRequired,
                                                                                        request.MaximumAllowed, null);
                            }

                            //
                            //  say we have changed the parent page and the request
                            uow.RegisterChanged(request.ParentPage.Template);
                        }
                    }

                    Page page = this.pages.CreateEntityInstance();
                    page.Name = request.PageName;
                    page.Template = theTemplate;
                    page.Parent = request.ParentPage;
                    page.CreatedBy = request.User;

                    //  create the page
                    uow.RegisterAdded(page);


                    //  commit the unit of work
                    uow.Commit();

                    return page;
                }
            }
            return null;
        }

        public void ValidatePublishedPageTemplates(CreatePageRequest request, IValidationProvider validation)
        {
            if(request.ParentPage != null && request.ParentPage.Template != null && request.MaximumAllowed.HasValue && request.ExistingPageTemplate != null)
            {
                //
                //  count them
                int c = request.ParentPage.ChildPages.Where(cp => cp.Template == request.ExistingPageTemplate).Count();
                if(c >= request.MaximumAllowed)
                    validation.AddError("MaximumAllowed", "Maximum Allowed amount has already been reached.");
            }
        }


        public void ValidatePublishedContentTemplates(CreateContentItemRequest request, IValidationProvider validation)
        {
            if (request.ParentPage.Template != null && request.MaximumAllowed.HasValue && request.ExistingContentTemplate != null)
            {
                //
                //  count them
                int c = request.ParentPage.ChildContent.Where(cp => cp.Template == request.ExistingContentTemplate).Count();
                if (c >= request.MaximumAllowed)
                    validation.AddError("MaximumAllowed", "Maximum Allowed amount has already been reached.");
            }
        }

        public ContentItem GetContentItemById(Guid contentItemId)
        {
            return this.contentItems[contentItemId];
        }

        public Page PublishPage(Guid id)
        {
            return this.PublishPages(new Guid[] {id}).First();
        }

        /// <summary>
        /// Publish content by Id
        /// </summary>
        /// <param name="id"></param>
        public IEnumerable<Page> PublishPages(Guid [] ids)
        {
            using (var uow = this.uowFactory.Begin())
            {
                List<Page> pages = new List<Page>();
                //  copy the pages into a list
                foreach(Guid id in ids)
                {
                    //  get the page
                    var page = this.pages[id];
                    if (page == null)
                        throw new BusinessRuleException("Page Not Found");
                    
                    //
                    //  the first thing we do 
                    pages.Add(page);
                }
                
                //  now we publish the page from the bottom up
                foreach(var page in pages.OrderByDescending(p => p.Level))
                {
                    //
                    //  ok, first thing we do is check to see if we to be deleted or not
                    if(page.Status == ContentStatus.AwaitingDelete || page.Status == ContentStatus.PublishedAndAwaitingDelete)
                    {
                        //  yup, we are, so do we have any children first of all that are not published
                        var pa = page.AllChildPages.FirstOrDefault(p => p.Status != ContentStatus.Deleted);
                        if(pa != null)
                            throw new BusinessRuleException("Page: '" + page.Name + "' contains a child page: '" + pa.Name + "' that has not been deleted.");

                        var ci = page.ChildContent.FirstOrDefault(c => c.Status != ContentStatus.Deleted && c.Status != ContentStatus.AwaitingDelete && c.Status != ContentStatus.PublishedAndAwaitingDelete);
                        if(ci != null)
                            throw new BusinessRuleException("Page: '" + page.Name + "' contains a child content item: '" + ci.Name + "' that has not been deleted.");

                        //
                        //  ok, we can delete this page
                        page.Status = ContentStatus.Deleted;
                        page.DatePublished = DateTime.Now;
                        
                    }
                    else
                    {
                        //
                        //  ok, time to publish
                        //  what we do is we call publish on the page and publish each of the child content item
                        page.DatePublished = DateTime.Now;
                        page.Status = ContentStatus.Published;
                        page.PublishedProperties = page.Properties;
                        page.PublishedTemplate = this.compilationProvider.GetSource(page.Template, this.pluginFactory);
                        var placeholders = this.compilationProvider.GetPlaceholders(page.Template, this.pluginFactory);
                        page.PublishedPlaceholders.Clear();
                        foreach(var key in placeholders.Keys)
                        {
                            page.PublishedPlaceholders.Add(key, placeholders[key]);
                        }
                        page.PublishedCss = page.GetCss();
                        page.PublishedScript = page.GetScript();
                        page.PublishedWorkflows = page.GetWorkflowConfiguration(true);
                    }

                    page.Template.DatePublished = DateTime.Now;

                    uow.RegisterChanged(page);
                    uow.RegisterChanged(page.Template);


                    //
                    //  we also have to iterate through each content item
                    foreach (ContentItem item in page.ChildContent)
                    {
                        this.PublishContentItem(page.DatePublished.Value, item);
                    }
                }




                uow.Commit();

                return pages;
            }
        }

        public void PublishContentItem(ContentItem item)
        {
            this.PublishContentItem(DateTime.Now, item);
        }

        private void PublishContentItem(DateTime datePublished, ContentItem item)
        {
            using (var uow = this.uowFactory.Begin())
            {
                //  and publish these
                switch (item.Status)
                {
                    case ContentStatus.Published:
                    case ContentStatus.Unpublished:
                        item.DatePublished = datePublished;
                        item.Status = ContentStatus.Published;
                        item.PublishedProperties = item.Properties;
                        break;
                    case ContentStatus.AwaitingDelete:
                    case ContentStatus.PublishedAndAwaitingDelete:
                        item.Status = ContentStatus.Deleted;
                        break;
                }


                //
                //  ok, save me
                uow.RegisterChanged(item);
                uow.Commit();
            }
        }

        public void UpdatePage(UpdatePageRequest updatePageRequest)
        {
            using(IUnitOfWork uow = this.uowFactory.Begin())
            {
                uow.RegisterChanged(updatePageRequest.Page);
                uow.Commit();
            }
        }

        public IEnumerable<Page> GetPagesByTags(string p)
        {
            return this.pages.FindByTags((p ?? "").Split(','));
        }

        public IEnumerable<ContentItem> GetAllConentItems()
        {
            return this.contentItems.All();
        }

        public IEnumerable<ContentItem> GetContentItemsByTags(string p)
        {
            throw new NotImplementedException();
        }

        public IEnumerable<ContentItem> GetContentByTemplate(Page parentPage, string template, string orderBy, string orderDirection, int? maximum, bool preview, ContentQueryOptions options)
        {
            return this.contentItems.FindByTemplate(parentPage, template, orderBy, orderDirection, maximum, preview, options)
            ;
        }




        public IEnumerable<Page> GetPagesByTemplate(string template)
        {
            return this.pages.FindByTemplate(template);
        }


        public void RenameContentItem(ContentItem contentItem, string newName)
        {
            if (string.IsNullOrEmpty(newName))
                throw new BusinessRuleException("Please enter a new name");

            if (contentItem == null)
                throw new ArgumentNullException("contentItem");

            if (contentItem.Page.ChildContent.Where(c => c.Name.ToLower() == newName.ToLower()).Any())
                throw new BusinessRuleException("Content item already exists");

            using (var uow = this.uowFactory.Begin())
            {
                contentItem.Name = newName;
                uow.RegisterChanged(contentItem);

                uow.Commit();
            }
        }



        public void ResequenceContent(Page page, ContentItem item, bool up)
        {
            if (page == null)
                throw new ArgumentNullException("page");

            if (item == null)
                throw new ArgumentNullException("item");

            if (!page.ChildContent.Where(c => c.Id == item.Id).Any())
                throw new BusinessRuleException("Content Item does not belong to the page");

            using (var uow = this.uowFactory.Begin())
            {
                if(up)
                {
                    var i = 0;
                    ContentItem lastContentItem = null;
                    foreach (var contentItem in page.ChildContent)
                    {
                        contentItem.Sequence = i;
                        uow.RegisterChanged(contentItem);

                        if (contentItem == item && lastContentItem != null)
                        {
                            lastContentItem.Sequence = i;
                            contentItem.Sequence = i - 1;
                        }

                        lastContentItem = contentItem;
                        i++;
                    }
                }

                if (!up)
                {
                    var i = int.MaxValue;
                    ContentItem lastContentItem = null;
                    foreach (var contentItem in page.ChildContent.Reverse())
                    {
                        contentItem.Sequence = i;
                        uow.RegisterChanged(contentItem);

                        if (contentItem == item && lastContentItem != null)
                        {
                            lastContentItem.Sequence = i;
                            contentItem.Sequence = i + 1;
                        }


                        lastContentItem = contentItem;
                        i--;
                    }
                }

                uow.Commit();
            }
        }

       
    }
}
